Capture native-precompile state changes in Firehose tracer#14
Conversation
Native precompiles (B-20 tokens, activation/policy registries) write storage and emit logs directly on the journal via EvmInternals, without executing SSTORE/LOG opcodes. The inspector captured storage in step_end (gated on the SSTORE opcode) and logs in log_full (the LOG opcode), so both were lost for precompile calls: the call frame carried 0 logs while the receipt carried them, panicking the receipt-log reconciliation, and storage writes vanished silently. Gather both in call_end, after process_journal_changes and before the frame is popped, so they attach to the precompile call that produced them. A storage_processed_up_to high-water-mark (advanced by step_end) prevents re-emitting opcode SSTOREs. revm's Inspector::log_full rustdoc anticipates exactly this: "This will not happen only if custom precompiles where logs will be gathered after precompile call." Balance, nonce and code changes were already captured via the journal-driven process_journal_changes path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maoueh
left a comment
There was a problem hiding this comment.
Approving since it touches only tests
| fn decode_fire_block(raw: &[u8]) -> pb::sf::ethereum::r#type::v2::Block { | ||
| use base64::Engine as _; | ||
| use prost::Message as _; | ||
|
|
||
| let text = std::str::from_utf8(raw).expect("FIRE output is UTF-8"); | ||
| let line = text | ||
| .lines() | ||
| .find(|l| l.starts_with("FIRE BLOCK ")) | ||
| .expect("a FIRE BLOCK line"); | ||
| let payload = line.split(' ').next_back().expect("payload token"); | ||
| let bytes = base64::engine::general_purpose::STANDARD | ||
| .decode(payload) | ||
| .expect("base64 payload"); | ||
| pb::sf::ethereum::r#type::v2::Block::decode(bytes.as_slice()).expect("protobuf Block") | ||
| } |
There was a problem hiding this comment.
I would ask to check if evm-firehose-tracer-rs doesn't have something for this already...
There was a problem hiding this comment.
Checked — firehose-tracer (evm-firehose-tracer-rs) exposes no FIRE BLOCK parser; InMemoryBuffer only offers get_bytes(), and the decoded Block is private to the Tracer. So the ~6-line decode_fire_block (split the base64 payload off the FIRE BLOCK line, prost-decode) stays. Happy to upstream a small parse_fire_block helper into firehose-tracer if you prefer reuse across crates.
| if gather { | ||
| insp.gather_precompile_storage_changes(&mut ctx); | ||
| insp.gather_precompile_logs(&mut ctx); | ||
| } |
There was a problem hiding this comment.
This doesn't excerise on_call_end which added the two gather, let's wire the test directly with correct implementation and not a fake try.
The panic check test case should be removed
There was a problem hiding this comment.
Done in 83df090. The test now goes through the real Inspector::call / call_end hooks (call_end is what runs the two gathers); the precompile body is simulated by writing the storage slot + log straight on the journal between the hooks, exactly as EvmInternals does with no opcode. Removed the should_panic test — the single test still covers the bug (without the log gather, on_tx_end panics on the call/receipt log mismatch).
| const PRECOMPILE: Address = Address::new([ | ||
| 0x84, 0x53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, | ||
| ]); | ||
|
|
||
| fn legacy_tx_event() -> firehose_tracer::types::TxEvent { |
There was a problem hiding this comment.
Test helpers should go below where they used ideally, will adapt our sf-skill for this.
There was a problem hiding this comment.
Done in 83df090 — drive_precompile_call / legacy_tx_event / decode_fire_block now sit below the #[test] that uses them.
Address review: exercise the production Inspector::call / call_end path (which runs the gathers) instead of calling the private gather helpers directly; drop the now-redundant should_panic test; move test helpers below their use site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
Native precompiles (B-20 tokens, the activation/policy registries) write storage and emit logs directly on the journal via
EvmInternals— noSSTORE/LOGopcode runs. The Firehose inspector captured:step_end, gated on theSSTOREopcodelog_full, fired by theLOGopcodeNeither fires for a precompile, so for any precompile call:
assign_ordinal_and_index_to_receipt_logspanics (mismatch between call logs and receipt logs: transaction has 0 call logs but 1 receipt logs)Observed on base-sepolia for txs to
0x8453000000000000000000000000000000000001(activation registry).Fix
Gather both in
call_end, afterprocess_journal_changesand before the frame is popped, so they attach to the precompile call that produced them:gather_precompile_logs— emits journal logslog_fullnever saw (trx_logs_counthigh-water-mark)gather_precompile_storage_changes— emitsStorageChangedjournal entriesstep_endnever saw. A newstorage_processed_up_tomark (advanced bystep_end) prevents re-emitting opcode SSTOREs; reverted calls have their entries truncated by revm so the clamp drops them.revm's
Inspector::log_fullrustdoc anticipates exactly this: "This will not happen only if custom precompiles where logs will be gathered after precompile call."Balance, nonce and code changes were already captured via the journal-driven
process_journal_changespath (verified against a live trace), so only storage + logs needed gathering.Tests
crates/firehose/src/inspector.rs:precompile_journal_storage_and_logs_are_captured— drives a tx whose only call writes a slot + emits a log directly on the journal, decodes theFIRE BLOCK, asserts the call carries the storage change (key/old/new) and the log.precompile_logs_missing_without_gather_panics—#[should_panic], pins the original bug.Full
reth-firehoselib suite: 20/20 pass. Mutation-checked: disabling either gather fails its assertion.🤖 Generated with Claude Code